﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.ComponentModel;

namespace Objects.Transactions
{
    internal class TransactionableRegistry : Registry
    {        
        public class Desc
        {
            public Key Key;
            public bool IsControl;
            public Transaction Owner;
            public object Object;
            public IChangingObserver Observer;
            public override string ToString()
            {
                if (Object != null)
                    return Object.ToString();
                else
                    return base.ToString();
            }
        }

        private new class Namespace : Dictionary<Guid, Desc>
        {

        }

        private class Descriptor
        {
            public override string ToString()
            {
                return Transaction.ToString();
            }
            public Transaction Transaction;
            public Dictionary<string, Namespace> Items;
            /// <summary>
            /// Удаленные
            /// </summary>
            public Stack<Desc> Removed;
            public void Remove(string Namespace, Guid key)
            {
                Desc d = Items[Namespace][key];
                d.Key.Namespace = Namespace;
                Removed.Push(d);
                Items[Namespace].Remove(key);
                if (Items[Namespace].Count == 0)
                    Items.Remove(Namespace);
            }
            public void Add(string Namespace, Guid key, Desc desc)
            {
                Namespace ns;
                if (!Items.TryGetValue(Namespace, out ns))
                    Items.Add(Namespace, ns = new Namespace());
                ns.AddOrSetValue(key, desc);
            }
            public bool Contains(Key key)
            {
                Namespace ns;
                return Items.TryGetValue(key.Namespace, out ns) && ns.ContainsKey(key.Id);
            }
            public bool TryGet(Key key, out object obj)
            {
                Namespace ns;
                Desc d;
                obj = null;
                if (Items.TryGetValue(key.Namespace, out ns) && ns.TryGetValue(key.Id, out d))
                {
                    obj = d.Object;
                    return true;
                }
                else
                    return false;
            }
        }
        private Dictionary<IChangingObserver, Key> obsToGuid = new Dictionary<IChangingObserver, Key>();
        private Transaction globalTransaction;
        private readonly Dictionary<Transaction, int> indxTrn = new Dictionary<Transaction, int>();
        private readonly List<Descriptor> trns = new List<Descriptor>();
        private readonly static Dictionary<Thread, TransactionableRegistry> ovrRegs = new Dictionary<Thread, TransactionableRegistry>();

        private void ImportFromInherit()
        {
            Registry tmpr;
            Thread t = Thread.CurrentThread;
            ovrRegs.Add(t, this);
            this.globalTransaction = new Transaction("global");
            if (regs.TryGetValue(t, out tmpr))
                foreach (var item in tmpr.All)
                    foreach (var item1 in item.Value)
                        this[item.Key, item1.Key] = item1.Value;
            else
                regs.Add(t, null);
            regs[t] = this;
        }

        protected new static TransactionableRegistry GetOrCreateRegistry()
        {
            TransactionableRegistry res;

            if (!ovrRegs.TryGetValue(Thread.CurrentThread, out res))
            {
                res = new TransactionableRegistry();
                res.ImportFromInherit();
            }
            return res;
        }
        public new static TransactionableRegistry Instance
        {
            get
            {
                return GetOrCreateRegistry();
            }
        }

        #region Поддержка транзакций
        internal void AddTransaction(Transaction tr)
        {
            if (indxTrn.ContainsKey(tr))
                throw new TransactionException("Ошибка регистрации новой транзакции: попытка повторной регистрации");
            else
            {
                indxTrn.Add(tr, trns.Count);
                trns.Add(
                    new Descriptor()
                    {
                        Transaction = tr,
                        Items = new Dictionary<string, Namespace>(),
                        Removed = new Stack<Desc>()
                    });
            }
        }

        /// <summary>
        /// Закрепление транзакции
        /// </summary>
        /// <param name="tr"></param>
        internal void CommitTrn(Transaction tr)
        {
            if (!indxTrn.ContainsKey(tr))
                throw new TransactionException("Ошибка закрепления: транзакция незарегистрированна.");
            else if (globalTransaction != tr)
            {
                int i;
                while ((i = trns.Count - 1) >= 0 && trns[i].Transaction != tr)
                    CommitLast();
                // trn[i].Transaction == tr (очевидно)
                CommitLast();
            }
        }

        /// <summary>
        /// Контракт: trn.Count > 0
        /// </summary>
        private void CommitLast()
        {
            int indexTrn = trns.Count - 1;
            if (indexTrn != 0)
            {
                TransferDowm(trns[indexTrn], trns[indexTrn - 1]);
                Descriptor td = trns[indexTrn];
                foreach (var item in td.Items
                        .SelectMany(s => s.Value)
                        .Select(s => s.Value.Observer)
                        .Union(td.Removed
                            .Select(s => s.Observer))
                        .Where(s => s != null))
                    UnregObserver(item, false);
                RemoveTrn(trns[indexTrn].Transaction, indexTrn);
            }
        }

        /// <summary>
        /// Добавление безхозного объекта (текущая транзакция становится его хозяином)
        /// Контракт: объект еще не зарегистрирован
        /// </summary>
        /// <param name="key"></param>
        /// <param name="Object"></param>
        private void AddOwnerlessObject(string Namespace, Guid key, object value)
        {
            IChangingObservable st = value as IChangingObservable;
            IChangingObserver ob;
            if (st != null)
            {
                ob = st.GetObserver();
                RegNewObserver(ob, Namespace, key);
            }
            else
                ob = null;
            int cnt = trns.Count - 1;
            trns[cnt].Add(Namespace, key,
                new Desc()
                {
                    Key = new Key() { Namespace = Namespace, Id = key },
                    Owner = trns[cnt].Transaction,
                    IsControl = true,
                    Object = value,
                    Observer = ob
                }
                );
        }

        /// <summary>
        /// Восходящая (от старших к младшим) передача объекта между двумя транзакциями
        /// <para>Контракт:</para>
        /// <para>trDest - текущая транзакция, </para>
        /// <para>в trDest нет данного объекта,</para>
        /// <para>в trSource объект существует и не удален</para>
        /// </summary>
        /// <param name="trSource">Транзакция-источник</param>
        /// <param name="trDest">Транзакция-назначение</param>
        /// <param name="key">Ключ объекта в передающей транзакции</param>
        private void TransferUp(Descriptor trSource, Descriptor trDest, string Namespace, Guid key)
        {
            Desc d = trSource.Items[Namespace][key];
            object obj = d.Object;
            ICloneable cl;
            IChangingObserver ob;
            if ((ob = d.Observer) != null)
                d.Observer.SetCheckPoint();
            else if ((cl = obj as ICloneable) != null)
                obj = cl.Clone();
            d.IsControl = false;
            trDest.Add(Namespace, key,
                new Desc()
                {
                    Key = new Key() { Namespace = Namespace, Id = key },
                    IsControl = true,
                    Object = obj,
                    Owner = d.Owner,
                    Observer = ob
                });
        }

        /// <summary>
        /// Ниcходящая (от младших к старшим) передача контекста между двумя транзакциями.
        /// Закрепление контекста.
        /// </summary>
        /// <param name="trSource"></param>
        /// <param name="trDest"></param>
        private void TransferDowm(Descriptor trSource, Descriptor trDest)
        {
            Desc d;
            Namespace ns;
            while (trSource.Removed.Count > 0)
            {
                d = trSource.Removed.Pop();
                if (trDest.Items.TryGetValue(d.Key.Namespace, out ns) && ns.ContainsKey(d.Key.Id))
                    trDest.Remove(d.Key.Namespace, d.Key.Id);
                else
                    trDest.Removed.Push(d);
            }
            foreach (var item in trSource.Items)
                foreach (var item1 in item.Value)
                {
                    if (!trDest.Items.TryGetValue(item.Key, out ns) || !ns.TryGetValue(item1.Key, out d))
                        trDest.Add(item.Key, item1.Key, d = new Desc()
                        {
                            Key = new Key() { Namespace = item.Key, Id = item1.Key },
                            Owner = (trSource.Transaction == item1.Value.Owner) ?
                                trDest.Transaction : item1.Value.Owner,
                            Object = item1.Value.Object,
                            Observer = item1.Value.Observer
                        });
                    d.IsControl = true;
                }
        }

        /// <summary>
        /// Откат транзакции
        /// </summary>
        /// <param name="tr"></param>
        internal void RollbackTrn(Transaction tr)
        {
            if (!indxTrn.ContainsKey(tr))
                throw new TransactionException("Ошибка отката: транзакция незарегистрирована.");
            else if (globalTransaction != tr)
                while (indxTrn.ContainsKey(tr))
                    RollbackLast();
        }

        private void RollbackFull()
        {
            if (trns.Count > 1)
                RollbackTrn(trns[1].Transaction);
        }

        private void RollbackLast()
        {
            int indexTrn = trns.Count - 1;
            if (indexTrn != 0)
            {
                Descriptor td = trns[indexTrn];
                foreach (var item in td.Items
                        .SelectMany(s => s.Value)
                        .Select(s => s.Value.Observer)
                        .Union(td.Removed
                            .Select(s => s.Observer))
                        .Where(s => s != null))
                    UnregObserver(item, true);
                RemoveTrn(td.Transaction, indexTrn);
            }
        }

        /// <summary>
        /// Удаление транзакции
        /// Контракт: ind - индект верхней тразакции
        /// </summary>
        /// <param name="tr"></param>
        /// <param name="ind"></param>
        private void RemoveTrn(Transaction tr, int ind)
        {
            Descriptor td = trns[ind];
            trns.RemoveAt(ind);
            indxTrn.Remove(tr);
        }
        #endregion

        public override void Clear()
        {
            RollbackFull();
            base.Clear();
        }

        /// <summary>
        /// Добавление объектов к реестру
        /// </summary>
        /// <param name="index"></param>
        /// <returns></returns>        

        public override object this[string Namespace, Guid key]
        {
            get
            {
                return GetObject(Namespace, key);
            }
            set
            {
                object res;
                if (TryGetObject(Namespace, key, out res, false))
                    throw new TransactionException("Попытка перезаписать зарегистрированный объект.");
                else
                    AddOwnerlessObject(Namespace, key, value);
            }
        }

        public override void Remove(string Namespace, Guid key)
        {
            GetObject(Namespace, key);
            int cnt = trns.Count - 1;
            if (cnt > 0)
                trns[cnt].Remove(Namespace, key);
            else
            {
                IChangingObserver ob;
                if ((ob = trns[cnt].Items[Namespace][key].Observer) != null)
                    UnregObserver(ob, false);
                trns[cnt].Items[Namespace].Remove(key);
            }
        }

        private void RegNewObserver(IChangingObserver ob, string Namespace, Guid key)
        {
            obsToGuid.Add(ob, new Key() { Namespace = Namespace, Id = key });
            ob.PropertyChanging += ob_PropertyChanging;
        }

        private void UnregObserver(IChangingObserver ob, bool withrollback)
        {
            //var key = obsToGuid[ob];
            if (withrollback)
            {
                if (!ob.RollbackToCheckPoint())
                {
                    obsToGuid.Remove(ob);
                    ob.PropertyChanging -= ob_PropertyChanging;
                    ob.Dispose();
                }
            }
            else
                ob.RemoveCheckPoint();
        }

        private bool TryGetObject(string Namespace, Guid key, out object obj, bool useExtReg)
        {
            Desc d = null;
            Namespace ns;
            Key k = new Key() { Namespace = Namespace, Id = key };
            obj = null;
            int cnt = trns.Count - 1;
            for (int i = cnt; i >= 0; i--)
                if (trns[i].Items.TryGetValue(Namespace, out ns) && ns.TryGetValue(key, out d))
                {
                    if (i < cnt) //Объект найден в нижележащей транзакции
                    {
                        TransferUp(trns[i], trns[cnt], Namespace, key);
                        d = trns[cnt].Items[Namespace][key];
                    }
                    break;
                }
                else if (trns[i].Removed.Any(s => s.Key == k))
                    return false;
            if (d != null)
                obj = d.Object;
            else if (useExtReg && extRegistry != null)
            {
                if ((obj = extRegistry[key]) != null)
                {
                    AddOwnerlessObject(Namespace, key, obj);
                    return TryGetObject(Namespace, key, out obj, false);
                }
            }
            else
                return false;
            return true;
        }

        private object GetObject(Key key)
        {
            object res;
            if (!TryGetObject(key.Namespace, key.Id, out res, true))
                throw new TransactionException("Объект не обнаружен.");
            return res;
        }

        private object GetObject(string Namespace, Guid key)
        {
            object res;
            if (!TryGetObject(Namespace, key, out res, true))
                throw new TransactionException("Объект не обнаружен.");
            return res;
        }

        public Transaction Current
        {
            get { return trns[trns.Count - 1].Transaction; }
        }

        private void ob_PropertyChanging(object sender, System.ComponentModel.PropertyChangingEventArgs e)
        {
            IChangingObserver ob = sender as IChangingObserver;
            var index = obsToGuid[ob];
            int cnt = trns.Count - 1;
            if (trns.Count > 1 && !trns[cnt].Contains(index))
            {
                GetObject(index);
            }
        }
    }
}
